/*	
	
	Copyright (c) 2007-2009 Ryan Christiansen
	
	This software is provided 'as-is', without any express or implied
	warranty. In no event will the authors be held liable for any damages
	arising from the use of this software.
	
	Permission is granted to anyone to use this software for any purpose,
	including commercial applications, and to alter it and redistribute it
	freely, subject to the following restrictions:
	
	1. The origin of this software must not be misrepresented; you must not
	claim that you wrote the original software. If you use this software
	in a product, an acknowledgment in the product documentation would be
	appreciated but is not required.
	
	2. Altered source versions must be plainly marked as such, and must not be
	misrepresented as being the original software.
	
	3. This notice may not be removed or altered from any source
	distribution.
	
*/
package funit.framework.writers
{
	import funit.framework.MessageString;
	import funit.framework.MessageUtils;
	
	import mx.collections.IList;
	import mx.formatters.DateFormatter;
	import mx.utils.StringUtil;
	
	import sv.utils.ClassUtil;
	
	public class MessageWriter
	{
		private static const DEFAULT_LINE_LENGTH:int = 78;
		
		private var _stream:String = "";
		private var _maxLineLength:int = DEFAULT_LINE_LENGTH;
		
		
		/**
		 * @param message
		 * @param messageParameters
		 */
		public function MessageWriter( message:String = null, messageParameters:Array = null )
		{
			if ( message )
			{
				this.writeMessageLine(0, message, messageParameters);
			}
		}
		
		
		/**
		 * @param message
		 * @param messageParameters
		 */
		public function write( message:String, ... messageParameters ) : void
		{
			if ( messageParameters.length > 0 )
			{
				_stream += StringUtil.substitute(message, messageParameters);
			}
			else
			{
				_stream += message;
			}
		}
		
		/**
		 * @param message
		 * @param messageParameters
		 */
		public function writeLine( message:String = "", ... messageParameters ) : void
		{
			if ( messageParameters.length > 0 )
			{
				message = StringUtil.substitute(message, messageParameters);
			}
			
			if ( message )
			{
				write(message);
			}
			
			write(MessageString.NewLine);
		}
		
		/**
		 * @param level
		 * @param message
		 * @param messageParameters
		 */
		public function writeMessageLine( level:int, message:String, ... messageParameters ) : void
		{
			while (level-- >= 0)
			{
				write("  ");
			}
			
			if ( messageParameters.length > 0 )
			{
				message = StringUtil.substitute(message, messageParameters);
			}
			
			writeLine(message);
		}
		
		/**
		 * @param prepend
		 */
		public function writePrepend( prepend:String ) : void
		{
			write(MessageString.PrependFormat, prepend);
		}
		
		/**
		 * @param connector
		 */
		public function writeConnector( connector:String ) : void
		{
			write(MessageString.ConnectorFormat, connector);
		}
		
		/**
		 * @param modifier
		 */
		public function writeModifier( modifier:String ) : void
		{
			write(MessageString.ModifierFormat, modifier);
		}
		
		/**
		 * @param value
		 * @param prepend
		 */
		public function writeExpectedLine( value:Object, prepend:String = null ) : void
		{
			write(MessageString.ExpectedPrefix);
			
			if ( prepend )
			{
				writePrepend(prepend);
			}
			
			writeValue(value);
			
			writeLine();
		}
		
		/**
		 * @param value
		 * @param prepend
		 */
		public function writeActualLine( value:Object, prepend:String = null ) : void
		{
			write(MessageString.ActualPrefix);
			
			if ( prepend )
			{
				writePrepend(prepend);
			}
			
			writeValue(value);
			
			writeLine();
		}
		
		/**
		 * @param mismatch
		 */
		public function writeCaretLine( mismatch:int ) : void
		{
			// We subtract 2 for the initial 2 blanks and add back 1 for the initial quote
			var count:int = MessageString.PrefixLength + mismatch - 2 + 1;
			var line:String = "";
			
			while (count-- > 0)
			{
				line += "-";
			}
			
			writeLine("  {0}^", line);
		}
		
		/**
		 * @param expected
		 * @param actual
		 */
		public function displayDifferences( expected:Object, actual:Object ) : void
		{
			writeExpectedLine(expected);
			writeActualLine(actual);
		}
		
		/**
		 * @param expected
		 * @param actual
		 * @param mismatch
		 * @param ignoreCase
		 * @param clipping
		 */
		public function displayStringDifferences( expected:String, actual:String, mismatch:int, ignoreCase:Boolean = false, clipping:Boolean = true ) : void
		{
			// Maximum string we can display without truncating
			var maxLength:int = _maxLineLength
				- MessageString.PrefixLength // Allow for prefix
				- 2; // 2 quotation marks
			
			if (clipping)
			{
				var clipStart:int = MessageUtils.calculateClipStart(expected, actual, mismatch, maxLength);
				
				expected = MessageUtils.clipString(expected, clipStart, maxLength);
				actual = MessageUtils.clipString(actual, clipStart, maxLength);
			}
			
			expected = MessageUtils.convertWhitespace(expected);
			actual = MessageUtils.convertWhitespace(actual);
			
			// The mismatch position may have changed due to clipping or white space conversion
			mismatch = MessageUtils.findMismatchPosition(expected, actual, 0, ignoreCase);
			
			write( MessageString.ExpectedPrefix );
			writeValue( expected );
			
			if (ignoreCase)
			{
				writeModifier( "ignoring case" );
			}
			
			writeLine();
			writeActualLine(actual);
			
			if (mismatch >= 0)
			{
				writeCaretLine(mismatch);
			}
		}
		
		/**
		 * @param value
		 */
		public function writeValue( value:Object ) : void
		{
			if (value == null)
			{
				writeNull();
			}
			else if (value is String)
			{
				writeString(String(value));
			}
			else if (value is Array || value is IList)
			{
				writeCollectionElements(value, 0, 10);
			}
			else if (value is Date)
			{
				writeDate(value as Date);
			}
			else if ( value is Number ||
					  value is Boolean )
			{
				writeValueType( value );
			}
			else
			{
				writeDefault(value);
			}
		}
		
		/**
		 * 
		 */
		protected function writeNull() : void
		{
			write(MessageString.NullFormat);
		}
		
		/**
		 * @param value
		 */
		protected function writeString( value:String ) : void
		{
			if ( value )
			{
				write(MessageString.StringFormat, MessageUtils.convertWhitespace(value));
			}
			else
			{
				write(MessageString.StringEmptyFormat);
			}
		}
		
		/**
		 * @param value
		 * @param start
		 * @param maxLength
		 */
		protected function writeCollectionElements( value:Object, start:int, maxLength:int ) : void
		{
			if ( getLength(value) <= 0 )
			{
				write(MessageString.CollectionEmptyFormat);
				return;
			}
			
			var count:int = 0;
			var index:int = 0;
			write("[ ");
			
			for each ( var entry:Object in value )
			{
				if ( index++ >= start )
				{
					if (count > 0)
					{
						write(", ");
					}
					
					writeValue(entry);
					
					if ( ++count >= maxLength )
					{
						break;
					}
				}
			}
			
			if ( index < value.length )
			{
				write("...");
			}
			
			write(" ]");
		}
		
		/**
		 * @param enumerable
		 * @return
		 */
		protected function getLength( enumerable:Object ) : int
		{
			if (enumerable is Array)
			{
				return (enumerable as Array).length;
			}
			else if (enumerable is IList)
			{
				return IList(enumerable).length;
			}
			else
			{
				return -1;
			}
		}
		
		/**
		 * @param value
		 */
		protected function writeDate( value:Date ) : void
		{
			var formatter:DateFormatter = new DateFormatter();
			formatter.formatString = MessageString.DateFormat;
			
			var dateString:String = formatter.format(value);
			
			// DateFormatter does not support milliseconds and is added post-format.
			// This is only written when a value is specified.
			if (value.milliseconds > 0)
			{
				var milliseconds:String = value.milliseconds.toString();
				
				while (milliseconds.length < 3)
				{
					milliseconds = "0" + milliseconds;
				}
				
				dateString += "." + milliseconds;
			}
			
			write(dateString);
		}
		
		/**
		 * @param value
		 */
		protected function writeValueType( value:Object ) : void
		{
			write( value.toString() );
		}
		
		/**
		 * @param value
		 */
		protected function writeDefault( value:Object ) : void
		{
			write(
				MessageString.DefaultFormat,
				ClassUtil.getQualifiedClassName(value)
			);
		}
		
		/**
		 * @return
		 */
		public function toString() : String
		{
			return _stream;
		}
		
		
		/**
		 * 
		 */
		public function get maxLineLength() : int
		{
			return _maxLineLength;
		}
		
		public function set maxLineLength( value:int ) : void
		{
			_maxLineLength = value;
		}
		
	}
	
}
